feat: replace SMTP with Resend, switch Slack to bot token, make thresholds opt-in#9
Conversation
…holds opt-in - Replace nodemailer/SMTP with Resend SDK for email alerts (1 env var vs 6) - Switch Slack from webhook to bot token + chat.postMessage for richer control - Make static thresholds opt-in (default 0 = disabled), z-score and trends still active - Pass DASHBOARD_URL through cron to alert links - Update README: section title, config defaults, alerting docs - Remove unused sendSlackResolution function Co-authored-by: Cursor <cursoragent@cursor.com>
Summary of ChangesHello @ofershap, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request refines the alerting infrastructure by upgrading email and Slack notification systems for improved efficiency and control. It also introduces more flexible threshold configurations, allowing static limits to be optionally enabled, thereby providing users with greater customization over their alert settings. These changes aim to simplify setup, enhance alert fidelity, and ensure relevant contextual links are always available. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces several significant improvements to the alerting and configuration systems. The migration from SMTP to Resend for emails and the switch to a bot token for Slack alerts are great changes that simplify configuration and enhance functionality. Making the static thresholds opt-in by default is also a sensible UX improvement.
My review includes a few suggestions to improve the robustness of the new alerting logic by adding more comprehensive error handling and logging. I've also identified a gap in the threshold detection feature where the maxTokensPerDay check, although configured, is not implemented in the detection engine. Addressing these points will make the system more reliable and complete.
| export async function sendSlackAlert( | ||
| anomaly: Anomaly, | ||
| incident: Incident, | ||
| options: { webhookUrl?: string; dashboardUrl?: string } = {}, | ||
| options: { dashboardUrl?: string } = {}, | ||
| ): Promise<boolean> { | ||
| const webhookUrl = options.webhookUrl ?? process.env.SLACK_WEBHOOK_URL; | ||
| if (!webhookUrl) return false; | ||
| const token = process.env.SLACK_BOT_TOKEN; | ||
| const channel = process.env.SLACK_CHANNEL_ID; | ||
| if (!token || !channel) return false; | ||
|
|
||
| const blocks = buildAlertBlocks(anomaly, incident, options.dashboardUrl); | ||
|
|
||
| const response = await fetch(webhookUrl, { | ||
| const response = await fetch(SLACK_API_URL, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| headers: { | ||
| "Content-Type": "application/json; charset=utf-8", | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| body: JSON.stringify({ | ||
| channel, | ||
| text: `${severityEmoji(anomaly.severity)} ${anomaly.message} — ${anomaly.userEmail}`, | ||
| blocks, | ||
| }), | ||
| }); | ||
|
|
||
| return response.ok; | ||
| } | ||
|
|
||
| export async function sendSlackResolution( | ||
| anomaly: Anomaly, | ||
| options: { webhookUrl?: string } = {}, | ||
| ): Promise<boolean> { | ||
| const webhookUrl = options.webhookUrl ?? process.env.SLACK_WEBHOOK_URL; | ||
| if (!webhookUrl) return false; | ||
|
|
||
| const response = await fetch(webhookUrl, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| text: `:white_check_mark: Resolved: ${anomaly.message} — ${anomaly.userEmail}`, | ||
| blocks: [ | ||
| { | ||
| type: "section", | ||
| text: { | ||
| type: "mrkdwn", | ||
| text: `:white_check_mark: *Resolved:* ${anomaly.message}\n*User:* ${anomaly.userEmail}`, | ||
| }, | ||
| }, | ||
| ], | ||
| }), | ||
| }); | ||
| if (!response.ok) return false; | ||
|
|
||
| return response.ok; | ||
| const data = (await response.json()) as { ok: boolean }; | ||
| return data.ok; | ||
| } |
There was a problem hiding this comment.
For better error handling and robustness, it's a good idea to wrap the fetch call in a try...catch block to handle network errors, similar to how sendEmailAlert is implemented. Additionally, when the response from Slack is not ok, the response body often contains a helpful error message. Logging this message would make debugging much easier.
export async function sendSlackAlert(
anomaly: Anomaly,
incident: Incident,
options: { dashboardUrl?: string } = {},
): Promise<boolean> {
const token = process.env.SLACK_BOT_TOKEN;
const channel = process.env.SLACK_CHANNEL_ID;
if (!token || !channel) return false;
const blocks = buildAlertBlocks(anomaly, incident, options.dashboardUrl);
try {
const response = await fetch(SLACK_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
channel,
text: `${severityEmoji(anomaly.severity)} ${anomaly.message} — ${anomaly.userEmail}`,
blocks,
}),
});
if (!response.ok) {
const errorText = await response.text().catch(() => "Could not read error body");
console.error(`[slack] Failed to send alert. Status: ${response.status}. Body: ${errorText}`);
return false;
}
const data = (await response.json()) as { ok: boolean; error?: string };
if (!data.ok) {
console.error(`[slack] Slack API error: ${data.error}`);
return false;
}
return true;
} catch (error) {
console.error("[slack] Exception while sending alert:", error);
return false;
}
}| ) { | ||
| anomalies.push({ | ||
| userEmail: r.email, | ||
| type: "threshold", |
There was a problem hiding this comment.
This file implements threshold checks for maxSpendCentsPerCycle and maxRequestsPerDay, but the check for maxTokensPerDay seems to be missing. The configuration for it exists and it's mentioned in the README, so it appears to be an incomplete feature. To complete the threshold detection, you should add logic to check for daily token usage against config.thresholds.maxTokensPerDay.
This would likely involve:
- Querying the
usage_eventstable to sumtotal_tokensper user for the current day. - Iterating through the results and creating an anomaly if a user's token count exceeds the configured threshold (and the threshold is > 0).
- Add scripts/generate-mock-db.ts: generates data/mock.db with 65 fake users, 30 days of activity, anomalies, billing groups, and analytics - Support DATABASE_PATH env var in db.ts for switching databases - Add npm run dev:mock and Dev Server (Mock Data) VS Code task - Commit mock.db (776KB) so cloners get demo data out of the box Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
||
| const email = `${first.toLowerCase()}.${last.toLowerCase()}@acme-corp.com`; | ||
| const role = i < 3 ? "owner" : "member"; | ||
| const userId = `usr_${Math.random().toString(36).slice(2, 14)}`; |
Check failure
Code scanning / CodeQL
Insecure randomness High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 3 months ago
In general, to fix insecure randomness, replace uses of Math.random() (or other non‑cryptographic PRNGs) in security‑sensitive contexts with Node’s crypto module (randomBytes, randomUUID, or getRandomValues in browsers). Ensure the output length/format stays compatible with existing code (e.g., string length, allowed characters, prefixes), so you do not break consuming logic.
For this specific case, the best fix is to replace Math.random().toString(36).slice(2, 14) with a cryptographically secure random string generator while keeping a similar length and character set. In Node we can use crypto.randomBytes and map the resulting bytes into a base‑36 character set. Since this is a small utility script, we should avoid adding third‑party dependencies and instead rely on Node’s built‑in crypto. Concretely:
- Add an import:
import { randomBytes } from "node:crypto"; - Define a helper function, e.g.
generateSecureId(length: number): string, that usesrandomBytesto produce a base‑36 string of the requested length. A simple approach is to generate bytes and for each byte takebyte % 36to index into"0123456789abcdefghijklmnopqrstuvwxyz", which avoids biases large enough to matter here and is clearly more secure thanMath.random(). - Replace the
userIdconstruction on line 253 withconst userId = \usr_${generateSecureId(12)}`;` to keep the same length (12 chars) and format.
All changes are confined to scripts/generate-mock-db.ts: one new import, one new helper function (placed near the other utility functions, e.g., around dateStr/isoNow), and the replacement of the Math.random() expression.
| @@ -2,6 +2,7 @@ | ||
| import { unlinkSync } from "node:fs"; | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { randomBytes } from "node:crypto"; | ||
|
|
||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| const DB_PATH = path.join(__dirname, "..", "data", "mock.db"); | ||
| @@ -235,6 +236,17 @@ | ||
| return new Date().toISOString().replace("T", " ").slice(0, 19); | ||
| } | ||
|
|
||
| function generateSecureId(length: number): string { | ||
| const chars = "0123456789abcdefghijklmnopqrstuvwxyz"; | ||
| const bytes = randomBytes(length); | ||
| let result = ""; | ||
| for (let i = 0; i < length; i++) { | ||
| const index = bytes[i] % chars.length; | ||
| result += chars[index]; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| function generateMembers(): Array<{ email: string; name: string; role: string; userId: string }> { | ||
| const members: Array<{ email: string; name: string; role: string; userId: string }> = []; | ||
| const usedNames = new Set<string>(); | ||
| @@ -250,7 +262,7 @@ | ||
|
|
||
| const email = `${first.toLowerCase()}.${last.toLowerCase()}@acme-corp.com`; | ||
| const role = i < 3 ? "owner" : "member"; | ||
| const userId = `usr_${Math.random().toString(36).slice(2, 14)}`; | ||
| const userId = `usr_${generateSecureId(12)}`; | ||
| members.push({ email, name: fullName, role, userId }); | ||
| } | ||
| return members; |
Detection was too noisy — model shift and drift alerts flagged normal usage patterns, request-count alerts ignored cost, and 23+ individual Slack messages bombarded the channel. Detection changes: - Remove model shift detection (using an expensive model isn't inherently bad) - Remove drift/P75 detection (just identified power users, not actionable) - Remove request-based z-score (high request count on cheap models is fine) - Add daily spend spike: flags when today's spend > 5x personal average - Add cycle spend outlier: flags when cycle spend > 10x active team median - Z-score now spend-only, computed against active users (spend > $0) - Minimum $50/day floor to avoid alerts on trivial amounts - Critical threshold raised to 3x multiplier (was 2x) Alerting changes: - Slack uses bot token + chat.postMessage (replaces webhook) - Batch alerts: ≤3 anomalies send individual messages, >3 sends summary - Summary auto-chunks long lists to respect Slack's block character limit - Add logging to Slack and email modules (missing config, send success/failure) - Remove dead sendSlackResolution function - Pass DASHBOARD_URL to alert links Config changes: - New trend settings: spendSpikeMultiplier, spendSpikeLookbackDays, cycleOutlierMultiplier - getConfig() merges stored config with defaults for safe migration Co-authored-by: Cursor <cursoragent@cursor.com>
- Add bin/create.mjs: `npx cursor-usage-tracker my-tracker` clones the repo, installs deps, copies .env.example, and prints next steps - Remove "private": true to allow npm publishing - Add "files" field to ship only the bin/ directory (keeps package tiny) - Add @semantic-release/npm for auto-publish on release - Add NPM_TOKEN to release workflow - Update README quick start with npx option Co-authored-by: Cursor <cursoragent@cursor.com>
|
🎉 This PR is included in version 1.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
RESEND_API_KEY). Same HTML template, samesendEmailAlertinterface.chat.postMessagefor richer control. Removed unusedsendSlackResolution.DASHBOARD_URLto alert links so Slack/email alerts link back to the dashboard.CURSOR_ANALYTICS_API_KEYreference.Test plan
npm run typecheckpassesRESEND_API_KEYis setchat.postMessageMade with Cursor